#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <gl/glew.h>
#include <gl/glut.h>
#include "ProjectMatrix.h"
#include "ViewMatrix.h"
#include "ModelMatrix.h"

#define PI 3.14

enum buttonCode {
	MenuExit,
	MenuSpeed0,
	MenuSpeed1,
	MenuSpeed2,
	MenuSpeed3,
};

//global

int menu;

float speed, angle = 0.0f;
float rotateX = 90.0f / 180.0f * PI, rotateY = 45.0f / 180.0f * PI;

long curTime = 0, preTime = 0;
char fps[15];
int frameCount = 0;

bool mouseDown = false;
int mouseX, mouseY;

GLuint program, vao;
GLint pm, vm, mm, posLoc, norLoc;
GLuint buffers[3];

ProjectMatrix projectMat;
ViewMatrix viewMat;
ModelMatrix modelMat;

GLfloat vertexs[] = {0.0f, 2.0f, 0.0f, 1.0f,
	0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 
	1.0f, 1.0f, 0.5f, 1.0f, 1.0f, 1.5f, 0.0f, 1.0f, 1.0f, 1.0f, -0.5f, 1.0f, 1.0f, 0.5f, 0.0f, 1.0f, 
	-1.0f, 1.0f, 0.5f, 1.0f, -1.0f, 1.5f, 0.0f, 1.0f, -1.0f, 1.0f, -0.5f, 1.0f, -1.0f, 0.5f, 0.0f, 1.0f, 
	3.0f, -0.01f, 3.0f, 1.0f, 3.0f, -0.01f, -3.0f, 1.0f, -3.0f, -0.01f, -3.0f, 1.0f, -3.0f, -0.01f, 3.0f, 1.0f};
GLfloat normals[] = {0.0f, 1.0f, 0.0f,
	0.0f, -1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, -1.0f, -1.0f, -1.0f, 0.0f,
	1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, -1.0f, 1.0f, -1.0f, 0.0f,
	-1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f,
	0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f};
GLuint indexs[] = {0,1,2, 0,2,3, 0,3,4, 0,4,1, 5,6,7, 5,8,7, 9,10,11, 9,11,12, 5,6,10, 5,10,9, 5,9,12, 5,12,8, 7,10,6, 7,11,10, 7,8,12, 7,12,11, 13,14,15, 13,15,16};

char *textFileRead(char *fn) {

	FILE *fp;
	char *content = NULL;

	int count=0;

	if (fn != NULL) {
		fp = fopen(fn, "rt");

		if (fp != NULL) {

			fseek(fp, 0, SEEK_END);
			count = ftell(fp);
			rewind(fp);

			if (count > 0) {
				content = (char *)malloc(sizeof(char) * (count+1));
				count = fread(content,sizeof(char),count,fp);
				content[count] = '\0';
			}
			fclose(fp);
		}
	}
	return content;
}

void setShaders() {

	do
	{
		GLuint v, f;
		char *vs, *fs;

		// Create shader handlers
		v = glCreateShader(GL_VERTEX_SHADER);
		f = glCreateShader(GL_FRAGMENT_SHADER); 

		// Read source code from files
		vs = textFileRead("helloworld.vert");
		fs = textFileRead("helloworld.frag");

		const char * vv = vs;
		const char * ff = fs;

		// Set shader source
		glShaderSource(v, 1, &vv, NULL);
		glShaderSource(f, 1, &ff, NULL);

		free(vs);free(fs);

		// Compile all shaders
		int status;
		glCompileShader(v);
		glCompileShader(f);

		glGetShaderiv(v,  GL_COMPILE_STATUS, &status);
		if(status != GL_TRUE) {
			int infologLength = 0;
			int charsWritten  = 0;
			char *infoLog;

			glGetShaderiv(v, GL_INFO_LOG_LENGTH, &infologLength);

			if (infologLength > 0)
			{
				infoLog = (char *)malloc(infologLength);
				glGetShaderInfoLog(v, infologLength, &charsWritten, infoLog);
				printf("VertexShaderError:\n%s\n",infoLog);
				free(infoLog);
			}
			break;
		}

		glGetShaderiv(f,  GL_COMPILE_STATUS, &status);
		if(status != GL_TRUE) {
			int infologLength = 0;
			int charsWritten  = 0;
			char *infoLog;

			glGetShaderiv(f, GL_INFO_LOG_LENGTH, &infologLength);

			if (infologLength > 0)
			{
				infoLog = (char *)malloc(infologLength);
				glGetShaderInfoLog(f, infologLength, &charsWritten, infoLog);
				printf("FragmentShaderError:\n%s\n",infoLog);
				free(infoLog);
			}
			break;
		}

		// Create the program
		program = glCreateProgram();

		// Attach shaders to program
		glAttachShader(program, v);
		glAttachShader(program, f);

		// Link and set program to use
		glLinkProgram(program);
		glGetShaderiv(program,  GL_LINK_STATUS, &status);
		if(status != GL_TRUE) {
			int infologLength = 0;
			int charsWritten  = 0;
			char *infoLog;

			glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infologLength);

			if (infologLength > 0)
			{
				infoLog = (char *)malloc(infologLength);
				glGetProgramInfoLog(program, infologLength, &charsWritten, infoLog);
				printf("ProgramLinkError:\n%s\n",infoLog);
				free(infoLog);
			}
			break;
		}

		pm = glGetUniformLocation(program, "pvm.project");
		vm = glGetUniformLocation(program, "pvm.view");
		mm = glGetUniformLocation(program, "pvm.model");

		glGenVertexArrays(1, &vao);
		glBindVertexArray(vao);

		glGenBuffers(3, buffers);

		glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
		glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 17 * 4, vertexs, GL_STATIC_DRAW);
		posLoc = glGetAttribLocation(program, "position");
		glEnableVertexAttribArray(posLoc);
		glVertexAttribPointer(posLoc, 4, GL_FLOAT, GL_FALSE, 0, 0);

		glBindBuffer(GL_ARRAY_BUFFER, buffers[1]);
		glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 17 * 3, normals, GL_STATIC_DRAW);
		norLoc = glGetAttribLocation(program, "normal");
		glEnableVertexAttribArray(norLoc);
		glVertexAttribPointer(norLoc, 3, GL_FLOAT, GL_TRUE, 0, 0);

		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[2]);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * 18 * 3, indexs, GL_STATIC_DRAW);

		glBindVertexArray(0);

	} while (false);
}

void init() {
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_LIGHTING);
	setShaders();
}

void mouseMove(int x, int y) { 	
	if(mouseDown) {
		rotateX += (x - mouseX) * 0.005f;
		rotateY -= (y - mouseY) * 0.005f;
		mouseX = x;
		mouseY = y;
	}
}

void mouseButton(int button, int state, int x, int y) {
	if (button == GLUT_LEFT_BUTTON) {
		if (state == GLUT_DOWN) {
			mouseX = x;
			mouseY = y;
			mouseDown = true;
		}
		else  {
			mouseDown = false;
		}
	}
}

void menuCallback(int code) {
	switch (code) {
	case MenuExit:
		glDeleteProgram(program);
		exit(0);
		break;
	case MenuSpeed0:
		speed = 0.0f;
		break;
	case MenuSpeed1:
		speed = 0.1f;
		break;
	case MenuSpeed2:
		speed = 0.2f;
		break;
	case MenuSpeed3:
		speed = 0.4f;
		break;
	default:
		break;
	}
}

void createMenu() {
	menu = glutCreateMenu(menuCallback);

	int speedMenu = glutCreateMenu(menuCallback);
	glutAddMenuEntry("stop", MenuSpeed0);
	glutAddMenuEntry("slow", MenuSpeed1);
	glutAddMenuEntry("middle", MenuSpeed2);
	glutAddMenuEntry("fast", MenuSpeed2);

	glutSetMenu(menu);
	glutAddSubMenu("Speed", speedMenu);
	glutAddMenuEntry("Exit", MenuExit);

	glutAttachMenu(GLUT_RIGHT_BUTTON);
}

void renderScene(void) {
	curTime = glutGet(GLUT_ELAPSED_TIME);
	
	if(curTime - preTime > 1000) {
		sprintf(fps, "FPS: %d", frameCount);
		frameCount = 0;
		preTime = curTime;
	}
	else {
		frameCount ++;
	}

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	modelMat.loadIdentity();
	
	viewMat.lookAt(8.0f * sin(rotateY) * cos(rotateX), 8.0f * cos(rotateY), 8.0f * sin(rotateY) * sin(rotateX), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,  0.0f);
	
	float positionLight[] = {8.0f, 8.0f, 8.0f, 1.0f};
	float ambientLight[] = {0.2f, 0.2f, 0.2f, 0.2f};
	float diffuseLight[] = {0.8f, 0.8f, 0.8f, 0.8f};
	float specularLight[] = {1.0f, 1.0f, 1.0f, 1.0f};
	float specular[] = {1.0f, 1.0f, 1.0f, 1.0f};
	int shininess = 128;

	if(!mouseDown) {
		angle += speed;
	}
	modelMat.translate(0.0f, -0.5f, 0.0f);
	modelMat.rotate(angle - angle / 360.0f, 0.0f, 1.0f, 0.0f);

	glUniformMatrix4fv(pm, 1, GL_TRUE, projectMat.matrix[0]);
	glUniformMatrix4fv(vm, 1, GL_TRUE, viewMat.matrix[0]);
	glUniformMatrix4fv(mm, 1, GL_TRUE, modelMat.matrix[0]);

	glUseProgram(program);
	glBindVertexArray(vao);
	glDrawElements(GL_TRIANGLES, 18*3, GL_UNSIGNED_INT, 0);

	glutSwapBuffers();
}

void changeSize(int w, int h) {
	if (h == 0)
		h = 1;
	float ratio =  w * 1.0 / h;
	glViewport(0, 0, w, h);
	projectMat.perspective(45.0f, ratio, 0.1f, 100.0f);
}

int main(int argc, char **argv) {
	
	// init GLUT and create window
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
	glutInitWindowPosition(200,0);
	glutInitWindowSize(600, 600);
	glutCreateWindow("GLSL Framework");
	glewInit();

	// register callbacks
	glutDisplayFunc(renderScene);
	glutReshapeFunc(changeSize);
	glutIdleFunc(renderScene);
	
	// mouse function
	glutMouseFunc(mouseButton);
	glutMotionFunc(mouseMove);

	//create menu
	createMenu();

	// OpenGL init
	init();

	// enter GLUT event processing cycle
	glutMainLoop();

	return 1;
}